#!/usr/bin/env python
# -*- coding: utf-8 -*-

import sqlalchemy
import sqlalchemy.orm
from sqlalchemy.dialects import sqlite

import alembic
import alembic.config

import threading

import farado.logger
from farado.logger import logger
from farado.database.raw_querier import RawQuerier

from farado.items.field import Field
from farado.items.field_kind import FieldKind
from farado.items.field_kind_value import FieldKindValue
from farado.items.file import File
from farado.items.issue import Issue
from farado.items.issue_change import IssueChange
from farado.items.version import Version
from farado.items.issue_kind import IssueKind
from farado.items.comment import Comment
from farado.items.project import Project
from farado.items.user import User
from farado.items.workflow import Workflow
from farado.items.state import State
from farado.items.edge import Edge
from farado.items.role import Role
from farado.items.rule import Rule
from farado.items.user_role import UserRole
from farado.items.board import Board
from farado.items.board_column import BoardColumn
from farado.items.menu_item import MenuItem
from farado.items.message import Message



class MetaItemManager:
    def __init__(self, database_connection_string):
        self.engine = sqlalchemy.create_engine(database_connection_string)
        has_tables = self.engine.table_names()
        self.metadata = sqlalchemy.MetaData()
        self.create_tables()
        self.map_tables()
        self.metadata.create_all(self.engine)

        # Фиксация идентификатора версии БД
        if not has_tables:
            logger.warning('Создана новая база данных')
            alembic_config = alembic.config.Config("alembic.ini")
            alembic.command.stamp(alembic_config, "head")
            farado.logger.set_config()
            logger.info('Зафиксирован идентификатор версии БД')

        self.mutex = threading.RLock()
        self.session = sqlalchemy.orm.Session(self.engine)
        self.raw_querier = RawQuerier(
            self.engine,
            self.metadata,
            self.session,
            self.mutex,
        )

    def create_tables(self):
        self.projects_table = sqlalchemy.Table('projects'
            , self.metadata
            , sqlalchemy.Column('id', sqlalchemy.Integer, primary_key=True)
            , sqlalchemy.Column('caption', sqlalchemy.String)
            , sqlalchemy.Column('content', sqlalchemy.String)
            , sqlalchemy.Column('issues_view_settings', sqlalchemy.String)
        )

        self.versions_table = sqlalchemy.Table('versions'
            , self.metadata
            , sqlalchemy.Column('id', sqlalchemy.Integer, primary_key=True)
            , sqlalchemy.Column('caption', sqlalchemy.String)
            , sqlalchemy.Column('content', sqlalchemy.String)
            , sqlalchemy.Column('project_id',
                sqlalchemy.ForeignKey('projects.id', ondelete="CASCADE"), index=True)
            , sqlalchemy.Column('start_date', sqlalchemy.Date)
            , sqlalchemy.Column('release_date', sqlalchemy.Date)
        )

        self.workflows_table = sqlalchemy.Table('workflows'
            , self.metadata
            , sqlalchemy.Column('id', sqlalchemy.Integer, primary_key=True)
            , sqlalchemy.Column('caption', sqlalchemy.String)
            , sqlalchemy.Column('description', sqlalchemy.String)
        )

        self.states_table = sqlalchemy.Table('states'
            , self.metadata
            , sqlalchemy.Column('id', sqlalchemy.Integer, primary_key=True)
            , sqlalchemy.Column('caption', sqlalchemy.String)
            , sqlalchemy.Column('description', sqlalchemy.String)
            , sqlalchemy.Column('workflow_id',
                sqlalchemy.ForeignKey('workflows.id', ondelete="CASCADE"), index=True)
            , sqlalchemy.Column('weight', sqlalchemy.Integer)
            , sqlalchemy.Column('order', sqlalchemy.Integer)
            , sqlalchemy.Column('is_archive', sqlalchemy.Boolean)
        )

        self.edges_table = sqlalchemy.Table('edges'
            , self.metadata
            , sqlalchemy.Column('id', sqlalchemy.Integer, primary_key=True)
            , sqlalchemy.Column('from_state_id', sqlalchemy.Integer, index=True)
            , sqlalchemy.Column('to_state_id', sqlalchemy.Integer, index=True)
            , sqlalchemy.Column('workflow_id',
                sqlalchemy.ForeignKey('workflows.id', ondelete="CASCADE"), index=True)
        )

        self.issues_table = sqlalchemy.Table('issues'
            , self.metadata
            , sqlalchemy.Column('id', sqlalchemy.Integer, primary_key=True)
            , sqlalchemy.Column('issue_kind_id', sqlalchemy.Integer)
            , sqlalchemy.Column('parent_id', sqlalchemy.ForeignKey('issues.id'), index=True)
            , sqlalchemy.Column('project_id', sqlalchemy.ForeignKey('projects.id'), index=True)
            , sqlalchemy.Column('state_id', sqlalchemy.ForeignKey('states.id'), index=True)
            , sqlalchemy.Column('caption', sqlalchemy.String)
            , sqlalchemy.Column('content', sqlalchemy.String)
            , sqlalchemy.Column('version_id', sqlalchemy.ForeignKey('versions.id'), index=True)
        )

        self.issue_changes_table = sqlalchemy.Table('issue_changes'
            , self.metadata
            , sqlalchemy.Column('id', sqlalchemy.Integer, primary_key=True)
            , sqlalchemy.Column('issue_id', sqlalchemy.ForeignKey('issues.id'), index=True)
            , sqlalchemy.Column('diff', sqlalchemy.String)
            , sqlalchemy.Column('user_id',
                sqlalchemy.ForeignKey('users.id', ondelete="CASCADE"), index=True)
            , sqlalchemy.Column('date_time', sqlalchemy.DateTime)
        )

        self.issue_kinds_table = sqlalchemy.Table('issue_kinds'
            , self.metadata
            , sqlalchemy.Column('id', sqlalchemy.Integer, primary_key=True)
            , sqlalchemy.Column('caption', sqlalchemy.String)
            , sqlalchemy.Column('workflow_id', sqlalchemy.ForeignKey('workflows.id'), index=True)
            , sqlalchemy.Column('default_state_id', sqlalchemy.ForeignKey('states.id'), index=True)
            , sqlalchemy.Column('default_content', sqlalchemy.String)
        )

        self.fields_table = sqlalchemy.Table('fields'
            , self.metadata
            , sqlalchemy.Column('id', sqlalchemy.Integer, primary_key=True)
            , sqlalchemy.Column('issue_id',
                sqlalchemy.ForeignKey('issues.id', ondelete="CASCADE"), index=True)
            , sqlalchemy.Column('field_kind_id', sqlalchemy.Integer)
            , sqlalchemy.Column('value', sqlalchemy.String)
        )

        self.field_kinds_table = sqlalchemy.Table('field_kinds'
            , self.metadata
            , sqlalchemy.Column('id', sqlalchemy.Integer, primary_key=True)
            , sqlalchemy.Column('issue_kind_id',
                sqlalchemy.ForeignKey('issue_kinds.id', ondelete="CASCADE"), index=True)
            , sqlalchemy.Column('caption', sqlalchemy.String)
            , sqlalchemy.Column('value_type', sqlalchemy.Integer)
            , sqlalchemy.Column('description', sqlalchemy.String)
            , sqlalchemy.Column('is_system', sqlalchemy.Boolean)
        )

        self.field_kind_values_table = sqlalchemy.Table('field_kind_values'
            , self.metadata
            , sqlalchemy.Column('id', sqlalchemy.Integer, primary_key=True)
            , sqlalchemy.Column('field_kind_id',
                sqlalchemy.ForeignKey('field_kinds.id', ondelete="CASCADE"), index=True)
            , sqlalchemy.Column('value', sqlalchemy.String)
        )

        self.files_table = sqlalchemy.Table('files'
            , self.metadata
            , sqlalchemy.Column('id', sqlalchemy.Integer, primary_key=True)
            , sqlalchemy.Column('issue_id',
                sqlalchemy.ForeignKey('issues.id', ondelete="CASCADE"), index=True)
            , sqlalchemy.Column('comment_id',
                sqlalchemy.ForeignKey('comments.id', ondelete="CASCADE"), index=True)
            , sqlalchemy.Column('caption', sqlalchemy.String)
            , sqlalchemy.Column('name', sqlalchemy.String)
            , sqlalchemy.Column('path', sqlalchemy.String)
            , sqlalchemy.Column('description', sqlalchemy.String)
        )

        self.comments_table = sqlalchemy.Table('comments'
            , self.metadata
            , sqlalchemy.Column('id', sqlalchemy.Integer, primary_key=True)
            , sqlalchemy.Column('issue_id',
                sqlalchemy.ForeignKey('issues.id', ondelete="CASCADE"), index=True)
            , sqlalchemy.Column('user_id',
                sqlalchemy.ForeignKey('users.id', ondelete="CASCADE"), index=True)
            , sqlalchemy.Column('creation_datetime', sqlalchemy.DateTime)
            , sqlalchemy.Column('content', sqlalchemy.String)
        )

        self.users_table = sqlalchemy.Table('users'
            , self.metadata
            , sqlalchemy.Column('id', sqlalchemy.Integer, primary_key=True)
            , sqlalchemy.Column('login', sqlalchemy.String)
            , sqlalchemy.Column('first_name', sqlalchemy.String)
            , sqlalchemy.Column('middle_name', sqlalchemy.String)
            , sqlalchemy.Column('last_name', sqlalchemy.String)
            , sqlalchemy.Column('email', sqlalchemy.String)
            , sqlalchemy.Column('password_hash', sqlalchemy.String)
            , sqlalchemy.Column('need_change_password', sqlalchemy.Boolean)
            , sqlalchemy.Column('more_info', sqlalchemy.String)
            , sqlalchemy.Column('is_blocked', sqlalchemy.Boolean)
        )

        self.roles_table = sqlalchemy.Table('roles'
            , self.metadata
            , sqlalchemy.Column('id', sqlalchemy.Integer, primary_key=True)
            , sqlalchemy.Column('caption', sqlalchemy.String)
            , sqlalchemy.Column('icon', sqlalchemy.String)
        )

        self.rules_table = sqlalchemy.Table('rules'
            , self.metadata
            , sqlalchemy.Column('id', sqlalchemy.Integer, primary_key=True)
            , sqlalchemy.Column('caption', sqlalchemy.String)
            , sqlalchemy.Column('role_id',
                sqlalchemy.ForeignKey('roles.id', ondelete="CASCADE"), index=True)
            , sqlalchemy.Column('is_admin', sqlalchemy.Boolean)
            , sqlalchemy.Column('project_id', sqlalchemy.ForeignKey('projects.id'), index=True)
            , sqlalchemy.Column('project_rights', sqlalchemy.Integer)
            , sqlalchemy.Column('issue_kind_id', sqlalchemy.ForeignKey('issue_kinds.id'), index=True)
            , sqlalchemy.Column('workflow_id', sqlalchemy.ForeignKey('workflows.id'), index=True)
            , sqlalchemy.Column('issue_rights', sqlalchemy.Integer)
        )

        self.menu_items_table = sqlalchemy.Table('menu_items'
            , self.metadata
            , sqlalchemy.Column('id', sqlalchemy.Integer, primary_key=True)
            , sqlalchemy.Column('caption', sqlalchemy.String)
            , sqlalchemy.Column('link', sqlalchemy.String)
            , sqlalchemy.Column('icon', sqlalchemy.String)
            , sqlalchemy.Column('role_id',
                sqlalchemy.ForeignKey('roles.id', ondelete="CASCADE"), index=True)
        )

        self.user_roles_table = sqlalchemy.Table('user_roles'
            , self.metadata
            , sqlalchemy.Column('id', sqlalchemy.Integer, primary_key=True)
            , sqlalchemy.Column('user_id',
                sqlalchemy.ForeignKey('users.id', ondelete="CASCADE"), index=True)
            , sqlalchemy.Column('role_id',
                sqlalchemy.ForeignKey('roles.id', ondelete="CASCADE"), index=True)
        )

        self.messages_table = sqlalchemy.Table('messages'
            , self.metadata
            , sqlalchemy.Column('id', sqlalchemy.Integer, primary_key=True)
            , sqlalchemy.Column('sender_id',
                sqlalchemy.ForeignKey('users.id', ondelete="CASCADE"), index=True)
            , sqlalchemy.Column('receiver_id',
                sqlalchemy.ForeignKey('users.id', ondelete="CASCADE"), index=True)
            , sqlalchemy.Column('date_time', sqlalchemy.DateTime)
            , sqlalchemy.Column('caption', sqlalchemy.String)
            , sqlalchemy.Column('content', sqlalchemy.String)
            , sqlalchemy.Column('was_read', sqlalchemy.Boolean)
            , sqlalchemy.Column('issue_change_id',
                sqlalchemy.ForeignKey('issue_changes.id', ondelete="CASCADE"))
        )

        self.boards_table = sqlalchemy.Table('boards'
            , self.metadata
            , sqlalchemy.Column('id', sqlalchemy.Integer, primary_key=True)
            , sqlalchemy.Column('caption', sqlalchemy.String)
            , sqlalchemy.Column('description', sqlalchemy.String)
            , sqlalchemy.Column('workflow_id', sqlalchemy.ForeignKey('workflows.id'), index=True)
            , sqlalchemy.Column('filter_json', sqlalchemy.String)
        )

        self.board_columns_table = sqlalchemy.Table('board_columns'
            , self.metadata
            , sqlalchemy.Column('id', sqlalchemy.Integer, primary_key=True)
            , sqlalchemy.Column('board_id', sqlalchemy.ForeignKey('boards.id'), index=True)
            , sqlalchemy.Column('state_id', sqlalchemy.ForeignKey('states.id'), index=True)
            , sqlalchemy.Column('caption', sqlalchemy.String)
            , sqlalchemy.Column('order', sqlalchemy.Integer)
        )

    def map_tables(self):
        sqlalchemy.orm.mapper(Project, self.projects_table,
            properties={
                'issues': sqlalchemy.orm.relationship(
                    Issue,
                    lazy='noload'),
                'versions': sqlalchemy.orm.relationship(
                    Version,
                    cascade='all,delete',
                    backref="project",
                    lazy='immediate'),
            }
        )
        sqlalchemy.orm.mapper(Version, self.versions_table,
            properties={
                'issues': sqlalchemy.orm.relationship(
                    Issue,
                    lazy='noload')
            }
        )
        sqlalchemy.orm.mapper(Workflow, self.workflows_table,
            properties={
                'states': sqlalchemy.orm.relationship(
                    State,
                    cascade='all,delete',
                    backref="state_workflow",
                    lazy='immediate'),
                'edges': sqlalchemy.orm.relationship(
                    Edge,
                    cascade='all,delete',
                    backref="edge_workflow",
                    lazy='immediate'),
                'issue_kinds': sqlalchemy.orm.relationship(
                    IssueKind,
                    lazy='noload'),
            }
        )
        sqlalchemy.orm.mapper(State, self.states_table)
        sqlalchemy.orm.mapper(Edge, self.edges_table)
        sqlalchemy.orm.mapper(Issue, self.issues_table,
            properties={
                'fields': sqlalchemy.orm.relationship(
                    Field,
                    cascade='all,delete',
                    backref="issue",
                    lazy='immediate')
                , 'files': sqlalchemy.orm.relationship(
                    File,
                    cascade='all,delete',
                    backref="issue",
                    lazy='immediate')
                , 'comments': sqlalchemy.orm.relationship(
                    Comment,
                    cascade='all,delete',
                    backref="issue",
                    lazy='immediate')
                , 'children': sqlalchemy.orm.relationship(
                    Issue,
                    lazy='noload')
                , 'changes': sqlalchemy.orm.relationship(
                    IssueChange,
                    cascade='all,delete',
                    backref="issue",
                    lazy='immediate')
            }
        )
        sqlalchemy.orm.mapper(IssueChange, self.issue_changes_table)
        sqlalchemy.orm.mapper(IssueKind, self.issue_kinds_table,
            properties={
                'field_kinds': sqlalchemy.orm.relationship(
                    FieldKind,
                    cascade='all,delete',
                    backref="issue_kind",
                    lazy='immediate')
            }
        )
        sqlalchemy.orm.mapper(Field, self.fields_table)
        sqlalchemy.orm.mapper(FieldKind, self.field_kinds_table,
            properties={
                'values': sqlalchemy.orm.relationship(
                    FieldKindValue,
                    cascade='all,delete',
                    backref="field_kind",
                    lazy='immediate')
            }
        )
        sqlalchemy.orm.mapper(FieldKindValue, self.field_kind_values_table)
        sqlalchemy.orm.mapper(File, self.files_table)
        sqlalchemy.orm.mapper(Comment, self.comments_table,
            properties={
                'files': sqlalchemy.orm.relationship(
                    File,
                    cascade='all,delete',
                    backref="comment",
                    lazy='immediate')
            }
        )
        sqlalchemy.orm.mapper(User, self.users_table,
            properties={
                'user_roles': sqlalchemy.orm.relationship(
                    UserRole,
                    cascade='all,delete',
                    backref="user",
                    lazy='immediate',
                ),
                'issue_changes': sqlalchemy.orm.relationship(
                    IssueChange,
                    lazy='noload',
                ),
            }
        )
        sqlalchemy.orm.mapper(Role, self.roles_table,
            properties={
                'rules': sqlalchemy.orm.relationship(
                    Rule,
                    cascade='all,delete',
                    backref="role",
                    lazy='immediate'),
                'user_roles': sqlalchemy.orm.relationship(
                    UserRole,
                    cascade='all,delete',
                    backref="role",
                    lazy='immediate'),
                'menu_items': sqlalchemy.orm.relationship(
                    MenuItem,
                    cascade='all,delete',
                    backref="role",
                    lazy='immediate'),
            }
        )
        sqlalchemy.orm.mapper(Rule, self.rules_table)
        sqlalchemy.orm.mapper(MenuItem, self.menu_items_table)
        sqlalchemy.orm.mapper(UserRole, self.user_roles_table)
        sqlalchemy.orm.mapper(Message, self.messages_table)
        sqlalchemy.orm.mapper(Board, self.boards_table,
            properties={
                'board_columns': sqlalchemy.orm.relationship(
                    BoardColumn,
                    cascade='all,delete',
                    backref="column_board",
                    lazy='immediate'),
            }
        )
        sqlalchemy.orm.mapper(BoardColumn, self.board_columns_table)



    def add_item(self, item):
        with self.mutex:
            with self.session.begin():
                self.session.add(item)
                self.session.flush()
                self.session.refresh(item)
                self.session.expunge_all()

    def add_items(self, items):
        with self.mutex:
            with self.session.begin():
                self.session.add_all(items)
                self.session.flush()
                self.session.refresh(items)
                self.session.expunge_all()

    def delete_item_by_id(self, item_type, id):
        with self.mutex:
            with self.session.begin():
                item = self.session.get(item_type, id)
                self.session.delete(item)

    def merge_item(self, item):
        with self.mutex:
            with self.session.begin():
                self.session.merge(item)
                self.session.expunge_all()

    def items(self, item_type):
        with self.mutex:
            with self.session.begin():
                statement = sqlalchemy.select(item_type)
                result = self.session.execute(statement).scalars().all()
                self.session.expunge_all()
                return result

    def ordered_items(
            self,
            item_type,
            order_by,
            is_order_ascending=True,
            slice_start=None,
            slice_stop=None,
            search_value=None,
            search_fields=[]):
        '''Get a sorted list of objects from the database as a fragment

        The fields of the objects satisfy the search conditions

        Parameters
        ----------
        item_type : python class
            Item type specifying in which table to search

        order_by : str
            Name of the table column by which sorting will be performed

        is_order_ascending : bool
            Sort order direction of the table by the specified column

        slice_start : int
            Start offset from the beginning of the data to form the target fragment

        slice_stop : int
            Stop offset from the beginning of the data to form the target fragment

        search_value : str
            Searching value string

        search_fields : list of str
            List of table columns in which searching will be performed

        Returns
        -------
        list
            List of objects received from the database
        '''
        with self.mutex:
            with self.session.begin():
                statement = sqlalchemy.select(item_type)

                if is_order_ascending:
                    statement = statement.order_by(order_by)
                else:
                    statement = statement.order_by(sqlalchemy.desc(order_by))

                if search_value:
                    is_filter_ready = False
                    for field in search_fields:
                        new_filter = sqlalchemy.sql.column(field).contains(search_value)
                        if not is_filter_ready:
                            filter = new_filter
                            is_filter_ready = True
                        else:
                            filter = sqlalchemy.or_(filter, new_filter)
                    if is_filter_ready:
                        statement = statement.filter(filter)

                if not slice_start is None:
                    statement = statement.slice(slice_start, slice_stop)

                result = self.session.execute(statement).scalars().all()
                self.session.expunge_all()
                return result

    def items_count(self, item_type):
        with self.mutex:
            with self.session.begin():
                result = self.session.query(sqlalchemy.func.count(item_type.id)).first()
                if not result:
                    return 0
                return result[0]

    #--------------------------------------------------------------------------#
    def issues(
            self,
            slice_start=None,
            slice_stop=None,
            kinds_ids=[],
            states_ids=[],
            parents_ids=[],
            projects_ids=[],
            versions_ids=[],
            caption_contains=None,
            field_contains={},
            is_order_ascending=True,
            ):
        '''Предоставляет доступ к объектам типа Issue.

        Параметры
        ----------
        slice_start : int
            Порядковый номер первого элемента возвращаемого набора данных (0 — первый элемент).

        slice_stop : int
            Порядковый номер элемента, до которого будет возвращен набор данных.

        kinds_ids : list<int>
            Перечень идентификаторов объектов типа IssueKind, по которым будет отфильтрован
            итоговый набор Issue.

        states_ids : list<int>
            Перечень идентификаторов объектов типа State, по которым будет отфильтрован
            итоговый набор Issue.

        parents_ids : list<int>
            Перечень идентификаторов объектов типа Issue, являющихся родительскими объектами для искомых Issue,
            по ним будет отфильтрован итоговый набор данных.

        projects_ids : list<int>
            Перечень идентификаторов объектов типа Project, по которым будет отфильтрован
            итоговый набор Issue.

        versions_ids : list<int>
            Перечень идентификаторов объектов типа Version, по которым будет отфильтрован
            итоговый набор Issue.

        caption_contains : string
            Сторка, наличие который проверяется в наименовании возвращаемых Issue.

        field_contains : dict<int, string>
            Словарь перечня полей Issue и значений, наличие которых в них проверяется, где
            ключ — FieldKind.id, значение — искомая строка или контейнер искомых идентификаторов.
            Итоговый набор Issue будет отфильтрован по данным критериям по логическому «И».

        is_order_ascending : bool
            Итоговый набор Issue будет отсортирован по наименованию:
            True — по возрастанию
            False — по убыванию.

        Возвращает
        -------
        count : int
            Полное кол-во элементов с учётом всех фильтров но без учёта разбиения результата на части.

        issues : list<Issue>
            Итоговый набор Issue.
        '''
        with self.mutex:
            with self.session.begin():
                statement = sqlalchemy.select(Issue)

                # Фильтрация по содержанию в наименовании issue текста
                if caption_contains:
                    filter = sqlalchemy.sql.column('caption').contains(caption_contains)
                    statement = statement.filter(filter)

                # Фильтрация по принадлежности к типам issue
                is_ready, filter = MetaItemHelper().make_ids_filter(kinds_ids, Issue.issue_kind_id)
                if is_ready:
                    statement = statement.filter(filter)

                # Фильтрация по принадлежности к состояниям
                is_ready, filter = MetaItemHelper().make_ids_filter(states_ids, Issue.state_id)
                if is_ready:
                    statement = statement.filter(filter)

                # Фильтрация по принадлежности родительским issue
                is_ready, filter = MetaItemHelper().make_ids_filter(parents_ids, Issue.parent_id)
                if is_ready:
                    statement = statement.filter(filter)

                # Фильтрация по принадлежности проектам
                is_ready, filter = MetaItemHelper().make_ids_filter(projects_ids, Issue.project_id)
                if is_ready:
                    statement = statement.filter(filter)

                # Фильтрация по принадлежности версиям
                is_ready, filter = MetaItemHelper().make_ids_filter(versions_ids, Issue.version_id)
                if is_ready:
                    statement = statement.filter(filter)

                # Фильтрация по содержанию текста в значении поля issue
                if field_contains:
                    statement = statement.join(Field)
                    for pair in field_contains.items():
                        field_kind_id = pair[0]
                        field_kind_value = pair[1]
                        if type(field_kind_value) == list:
                            is_ready, filter = MetaItemHelper().make_ids_filter(field_kind_value, Field.value)
                            if is_ready:
                                statement = statement.filter(
                                    sqlalchemy.and_(
                                        Field.field_kind_id == field_kind_id,
                                        filter
                                    )
                                )
                        else:
                            statement = statement.filter(
                                sqlalchemy.and_(
                                    Field.field_kind_id == field_kind_id,
                                    sqlalchemy.sql.column('value').contains(field_kind_value)
                                )
                            )

                # Сортировка по наименованию issue
                if is_order_ascending:
                    statement = statement.order_by('caption')
                else:
                    statement = statement.order_by(sqlalchemy.desc('caption'))

                count = self.session.execute(
                    sqlalchemy.select(sqlalchemy.func.count('id')).select_from(statement)
                ).first()[0]

                if not slice_start is None and not slice_stop is None:
                    statement = statement.slice(slice_start, slice_stop)

                issues = self.session.execute(statement).scalars().all()
                self.session.expunge_all()
                return (count, issues)

    #--------------------------------------------------------------------------#
    def issues_count(
            self,
            kinds_ids=[],
            states_ids=[],
            parents_ids=[],
            projects_ids=[],
            versions_ids=[],
            ):
        '''Предоставляет количество issues с учётом заданных фильтров.

        Параметры
        ----------
        kinds_ids : list<int>
            Перечень идентификаторов объектов типа IssueKind, используется для фильтрации результата.

        states_ids : list<int>
            Перечень идентификаторов объектов типа State, используется для фильтрации результата.

        parents_ids : list<int>
            Перечень идентификаторов объектов типа Issue, являющихся родительскими объектами для искомых Issue,
            используется для фильтрации результата.

        projects_ids : list<int>
            Перечень идентификаторов объектов типа Project, используется для фильтрации результата.

        versions_ids : list<int>
            Перечень идентификаторов объектов типа Version, используется для фильтрации результата.

        Возвращает
        -------
        int
            Количество Issues с учётом всех фильтров.
        '''
        with self.mutex:
            with self.session.begin():
                statement = self.session.query(sqlalchemy.func.count(Issue.id))

                # Фильтрация по принадлежности к типам issue
                is_ready, filter = MetaItemHelper().make_ids_filter(kinds_ids, Issue.issue_kind_id)
                if is_ready:
                    statement = statement.filter(filter)

                # Фильтрация по принадлежности к состояниям
                is_ready, filter = MetaItemHelper().make_ids_filter(states_ids, Issue.state_id)
                if is_ready:
                    statement = statement.filter(filter)

                # Фильтрация по принадлежности родительским issue
                is_ready, filter = MetaItemHelper().make_ids_filter(parents_ids, Issue.parent_id)
                if is_ready:
                    statement = statement.filter(filter)

                # Фильтрация по принадлежности проектам
                is_ready, filter = MetaItemHelper().make_ids_filter(projects_ids, Issue.project_id)
                if is_ready:
                    statement = statement.filter(filter)

                # Фильтрация по принадлежности версиям
                is_ready, filter = MetaItemHelper().make_ids_filter(versions_ids, Issue.version_id)
                if is_ready:
                    statement = statement.filter(filter)

                result = statement.first()[0]
                return result

    #--------------------------------------------------------------------------#
    def issues_by_ids(self, ids):
        with self.mutex:
            with self.session.begin():
                statement = sqlalchemy.select(Issue).filter(Issue.id.in_(ids))
                issues = self.session.execute(statement).scalars().all()
                self.session.expunge_all()
                # TODO: Временное решение. Сделать сортировку через sqlalchemy.
                result = []
                for id in ids:
                    result += [issue for issue in issues if id == issue.id]
                return result

    #--------------------------------------------------------------------------#
    def items_by_ids(self, item_type, ids):
        with self.mutex:
            with self.session.begin():
                statement = sqlalchemy.select(item_type).filter(
                    item_type.id.in_(ids)
                )
                issues = self.session.execute(statement).scalars().all()
                self.session.expunge_all()
                # TODO: Временное решение. Сделать сортировку через sqlalchemy.
                issues_dict = {issue.id: issue for issue in issues}
                result = [issues_dict[id] for id in ids if id in issues_dict]
                return result

    def item_by_id(self, item_type, id):
        with self.mutex:
            with self.session.begin():
                statement = sqlalchemy.select(item_type).filter_by(id=id)
                result = self.session.execute(statement).scalars().first()
                self.session.expunge_all()
                return result

    def item_by_value(self, item_type, value_name, value):
        with self.mutex:
            with self.session.begin():
                statement = sqlalchemy.select(item_type)
                statement = eval(f"statement.filter_by({value_name}=value)")
                result = self.session.execute(statement).scalars().first()
                self.session.expunge_all()
                return result

    def items_by_value(self, item_type, value_name, value):
        with self.mutex:
            with self.session.begin():
                statement = sqlalchemy.select(item_type)
                statement = eval(f"statement.filter_by({value_name}=value)")
                result = self.session.execute(statement).scalars().all()
                self.session.expunge_all()
                return result

    def item_by_values(self, item_type, values):
        with self.mutex:
            with self.session.begin():
                statement = sqlalchemy.select(item_type)
                for key, value in values.items():
                    statement = eval(f"statement.filter_by({key}=value)")
                result = self.session.execute(statement).scalars().first()
                self.session.expunge_all()
                return result

    def items_by_values(self, item_type, values):
        with self.mutex:
            with self.session.begin():
                statement = sqlalchemy.select(item_type)
                for key, value in values.items():
                    statement = eval(f"statement.filter_by({key}=value)")
                result = self.session.execute(statement).scalars().all()
                self.session.expunge_all()
                return result

    def items_ids(self, item_type):
        with self.mutex:
            with self.session.begin():
                statement = sqlalchemy.select(item_type.id)
                return [id[0] for id in self.session.execute(statement).all()]

    def items_ids_by_value(self, item_type, value_name, value):
        with self.mutex:
            with self.session.begin():
                statement = sqlalchemy.select(item_type.id)
                statement = eval(f"statement.filter_by({value_name}=value)")
                result = self.session.execute(statement).scalars().all()
                self.session.expunge_all()
                return result

    def issues_by_field(self, value, field_kind_id=None):
        with self.mutex:
            with self.session.begin():
                query = self.session.query(Issue).join(Field).where(Field.value == value)
                if not field_kind_id:
                    result = query.all()
                else:
                    result = query.where(Field.field_kind_id == field_kind_id).all()
                self.session.expunge_all()
                return result

    def roles_by_user(self, user_id=None):
        '''
        Query example:
            SELECT roles.id AS roles_id, roles.caption AS roles_caption
            FROM roles JOIN user_roles ON roles.id = user_roles.role_id
            WHERE user_roles.user_id = ?
        '''
        with self.mutex:
            with self.session.begin():
                query = self.session.query(Role).join(Role.user_roles).where(UserRole.user_id == user_id)
                result = query.all()
                self.session.expunge_all()
                return result

    def subissues_by_id(self, id):
        with self.mutex:
            with self.session.begin():
                result = self.session.query(Issue).where(Issue.parent_id == id).all()
                self.session.expunge_all()
                return result

class MetaItemHelper:
    def make_ids_filter(self, ids, property):
        is_condition_ready = False
        condition = None
        for id in ids:
            new_condition = (property == id)
            if not is_condition_ready:
                condition = new_condition
                is_condition_ready= True
            else:
                condition = sqlalchemy.or_(condition, new_condition)
        return (is_condition_ready, condition)

    def print_statement(self, statement):
        '''
        Отладочный метод для вывода итогового запроса в БД
        '''
        print(
            statement.compile(
                dialect=sqlite.dialect(),
                compile_kwargs={"literal_binds": True},
            )
        )
